六.控制转移
控制转移基本上可分为两大类:同一任务内的控制转移和任务间的控制转移(任务
切换)。同一任务内的控制转移又可分为:段内转移、特权级不变的段间转移和特
权级变换的段间转移。段内转移与实模式下相似,不涉及特权级变换和任务切换。
只有段间转移才涉及特权级变换和任务切换。本文介绍保护方式下的控制转移,
重点是任务内的特权级变换和任务间的切换。
这里下载本文所有源代码。
<一>任务内无特权级变换的转移
各种段内转移与实模式下相似,当然不涉及特权级变换和任务切换。只有各种形式
的段间转移才涉及特权级变换和任务切换。
1.段间转移指令
与实模式下相同,指令JMP、CALL和RET都具有段间转移的功能,指令INT和IRET总是
段间转移。此外,中断/异常也将引起段间转移。有时把这些具有段间转移功能的指
令统称为段间转移指令。
在保护模式下,段间转移的目标位置由选择子和偏移构成的地址表示,常把它称为
目标地址指针。在32位代码段中,上述指针内的偏移使用32位表示,这样的指针也
称为48位全指针。在实例二的32位代码段内就使用了48位全指针。在16位代码段中
,上述指针内的偏移只使用16位表示。
与实模式下相似,段间转移指令JMP和段间调用指令CALL还可分为段间直接转移和段
间间接转移两类。如果指令JMP和CALL在指令中直接含有目标地址指针,那么就是段
间直接转移;如果指令中含有指向包含目标地址指针的门描述符或TSS描述符的指针
,那么就是段间间接转移,这种指针只有选择子部分有效,指示调用门、任务门或
TSS描述符,而偏移部分不起作用。实际上,当段间转移指令JMP和段间调用指令CALL所
含指针的选择子部分指示代码段描述符,那么就是段间直接转移,偏移部分表示目
标代码段的入口点;当选择子部分指示门描述符或TSS描述符时,就是段间间接转移。
2.向目标代码段转移的步骤
处理器在执行上述段间转移指令向目标代码段实施转移的过程中,一般至少要经过如
下步骤:
(1)判断目标地址指针内的选择子指示的描述符是否为空描述符。空描述符是GDT中的
第0个描述符,是一个特殊的描述符。目标代码段描述符不能为空描述符,也即选择
子的高14位不能为0。
(2)从全局或局部描述符表内读出目标代码段描述符。由选择子内的TI位,确定使用
全局描述符表还是局部描述符表。
(3)根据情况,检测描述符类型是否正确;调整RPL。
(4)把目标代码段描述符内的有关内容装载到CS高速缓冲寄存器。
(5)判断目标地址指针内的偏移是否越出代码段的界限。目标地址指针内的偏移必须
不超过目标代码段界限。
(6)装载CS段寄存器和指令指针寄存器EIP;CPL存入CS内选择子的RPL字段。
上述步骤只是对转移过程的简单说明,实际的动作还要复杂。在把目标代码段描述符
内的有关内容转载到CS高速缓冲寄存器时,还要进行如下保护检测,其中的DPL表示
目标代码段描述符的特权级:
(1)对于非一致代码段,要求CPL=DPL,RPL<=DPL;对于一致代码段,要求CPL>=DPL。
(2)代码段必须存在,即描述符中的P位必须是1。
通常描述符特权级DPL规定了对应段的特权级。如果描述符描述的是数据段,那么DPL就
规定了访问该数据段的最外层特权级;如果描述符描述的是代码段,那么DPL就规定了
执行该代码段所需要的CPL。但从上述装载CS高速缓冲寄存器时进行的保护检测可见,
对于一致代码段,却要求CPL>=DPL,也就是说,一致代码段描述符中的DPL规定了可以转
移到一致代码段的最内层特权级。于是,3级的程序可以转移到任何一致的代码段,而0级
的程序只允许转移到DPL等于0的一致代码段。一致代码段描述符内DPL的这种解释,正好
与正常的DPL的解释相反。
一致的可执行段是一种特别的段。这种存储段,为在多个特权级执行的程序,提供对子例
程的共享支持,而不要求改变特权级。例如,通过把数值库例程放在一致的代码段中,可
以使不同级执行的程序共享数值库例程。这样,任何特权级的程序可以使用段间调用指令
,调用库中的例程,并在调用者所具有的特权级执行该例程。
3.任务内无特权级变换的转移
所谓任务内无特权级变换的转移指:在转移到新的代码段时,当前特权级CPL保持不变。
利用段间转移指令JMP、段间调用指令CALL和段间返回指令RET可实现任务内无特权级变换
的转移。利用INT指令和IRET指令也可实现任务内无特权级变换的转移。
(1)利用段间直接转移指令JMP或CALL
在执行段间转移指令JMP时,如果指令内所含指针指示一个代码段,那么就直接开始上述
向目标代码段转移的步骤;在执行段间调用指令CALL时,如果指令内所含指针指针指示一
个代码段,那么就把返回地址指针压栈,然后就直接开始上述向目标代码段转移的步骤。
顺利通过这几步(不调整RPL)后,就完成了任务内无特权级变换的转移。
由此可见,利用段间直接转移指令JMP或调用指令CALL可方便地进行任务内无特权级变换
的转移,但不能进行任务内特权级变换的转移。
(2)利用段间返回指令RET
在执行段间返回指令RET时,如果从堆栈中弹出的目标地址指针指示一个代码段,并且选
择子符合RPL=CPL的条件,那么就开始上述向目标代码段的转移步骤。顺利通过这几步后
,就完成了任务内无特权级变换的转移。
通常情况下,段间返回指令RET与段间调用指令CALL对应。在利用段间调用指令CALL以任
务内无特权级变换的方式转移到某个子程序后,在子程序内利用段间返回指令RET以任务
内无特权级变换的方式返回主程序。由于调用时无特权级变换,所以返回时也无特权级变
换,如果真是如此,那么必须能够满足条件RPL=CPL。
(3)利用调用门和其它途径
如何利用调用门实行和其它方法实现任务内无特权级变换的转移将在后面的文章中介绍。
4.装载数据段和堆栈段寄存器时的特权检测
上面简单地说明了把选择子装入代码段寄存器CS时为实现保护而进行的检测,下面也简单
地说明在把选择子装入数据段寄存器和堆栈段寄存器时要进行的检测。
在把选择子装入数据段寄存器DS、ES、FS或GS时,要进行如下检测:
(1)选择子不能为空;
(2)选择子指定的描述符必须是数据段描述符、可读可执行的代码段或一致可读的可执行
代码段的描述符;
(3)对于数据段和可读可执行代码段,要求CPL<=DPL,RPL<=DPL;
(4)对应的段必须存在。
若装入的选择子不满足上述要求,则会产生异常。
在把选择子装入堆栈段寄存器SS时要进行如下检测:
(1)选择子不能为空;
(2)选择子指定的描述符必须是可读写的数据段描述符;
(3)要求CPL=DPL=RPL;
(4)对应段必须存在。
若装入的选择子不满足上述条件,则在装入SS时就会引起异常。
<二>演示任务内无特权级变换转移的实例(实例三)
在实例二中,32位代码段到16位代码段的转移就是任务内无特权级转移的例子。
下面再给出一个用于演示任务内无特权级变换转移的实例。该实例使用了段间转移指令JMP、
段间调用指令CALL和段间返回指令RET实现同一任务内相同特权级的转移。该实例还建立并使
用了局部描述符表LDT。
1.实现步骤和源程序
实现步骤如下:(1)实模式下的初始化,包括对GDT和演示任务LDT的初始化,装载GDTR;(2)从
实模式切换到保护模式,处于0特权级;(3)装载LDTR,并设置堆栈;(4)利用段间转移指
令JMP实现从代码段K到同级代码段L的转移;(5)利用段间调用指令CALL调用同级代码段C中的
子程序D显示字符串信息;(6)利用段间调用指令CALL调用同级代码段C中的子程序H把十六进制
数转换成对应的ASCII码;(7)再利用段间调用指令CALL调用同级代码段C中的子程序D显示字符
串信息;(8)利用段间转移指令JMP实现从代码段L到代码段K的转移;(9)从保护模式切换到实
模式;(10)在实模式下结束程序。
该实例的逻辑功能是用十六进制数的形式显示代码段L的段界限的值。源程序如下:
INCLUDE 386SCD.INC
GDTSeg SEGMENT PARA USE16 'GDT'
GDT LABEL BYTE
DUMMY Desc <>
Normal Desc <0ffffh,,,ATDW,,>
CodeK Desc <0ffffh,,,ATCE,,>
LDTable Desc <LDTLen-1,,,ATLDT,,>
GDTLen = $-GDT
Normal_Sel = Normal-GDT
CodeK_Sel = CodeK-GDT
LDT_Sel = LDTable-GDT
GDTSeg ENDS
LDTSeg SEGMENT PARA USE16 'LDT'
LDT LABEL BYTE
CodeL Desc <CodeLLen-1,CodeLSeg,,ATCE,,>
CodeC Desc <CodeCLen-1,CodeCSeg,,ATCE,,>
VideoBuf Desc <0ffffh,0b800h,,ATDW,,>
ToLDT Desc <LDTLen-1,LDTSEG,,ATDR+DPL3,,>
MData Desc <MDataLen-1,MDataSeg,,ATDW+DPL3,,>
StackS Desc <TopOfS-1,StackSeg,,ATDWA,,>
LDTLen = $-LDT
LDNum = ($-LDT)/(SIZE Desc)
CodeL_Sel = CodeL-LDT+TIL
CodeC_Sel = CodeC-LDT+TIL
Video_Sel = VideoBuf-LDT+TIL
ToLDT_Sel = ToLDT-LDT+TIL
MData_Sel = MData-LDT+TIL+RPL3
Stack_Sel = StackS-LDT+TIL
LDTSeg ENDS
MDataSeg SEGMENT PARA USE16 'MDATA'
Message DB 'Value=',0
Buffer DB 80 DUP(0)
MDataLen = $
MDataSeg ENDS
StackSeg SEGMENT DWORD USE16 'STACK'
DW 512 DUP(?)
TopOfS = $
StackSeg ENDS
CodeCSeg SEGMENT PARA USE16 'CODEC'
ASSUME CS:CodeCSeg
DispMsg PROC FAR
mov ah,01001110b
Disp1: mov al,BYTE PTR fs:[si]
inc si
or al,al
jz Disp2
mov WORD PTR es:[di],ax
inc di
inc di
jmp Disp1
Disp2: ret
DispMsg ENDP
HToASCII PROC FAR
and al,00001111b
add al,90h
daa
adc al,40h
daa
ret
HToASCII ENDP
CodeCLen = $
CodeCSeg ENDS
CodeLSeg SEGMENT PARA USE16 'CODEL'
ASSUME CS:CodeLSeg
Virtual2 PROC FAR
mov ax,Video_Sel
mov es,ax
mov di,1986
mov ax,MData_Sel
mov fs,ax
mov si,OFFSET Message
CALL16 CodeC_Sel,DispMsg
mov ax,ToLDT_Sel
mov gs,ax
mov dx,WORD PTR gs:CodeL.LimitL
mov si,OFFSET Buffer
mov cx,4
Vir: rol dx,4
mov al,dl
CALL16 CodeC_Sel,HToASCII
mov BYTE PTR fs:[si],al
inc si
loop Vir
mov WORD PTR fs:[si],'H'
mov si,OFFSET Buffer
CALL16 CodeC_Sel,DispMsg
JUMP16 CodeK_Sel,Virtual3
CodeLLen = $
Virtual2 ENDP
CodeLSeg ENDS
CodeKSeg SEGMENT PARA USE16 'CODEK'
ASSUME CS:CodeKSeg
Virtual1 PROC FAR
mov ax,LDT_Sel
LLDT ax
mov ax,Stack_Sel
mov ss,ax
mov sp,OFFSET TopOfS
JUMP16 CodeL_Sel,Virtual2
Virtual3: mov ax,Normal_Sel
mov es,ax
mov fs,ax
mov gs,ax
mov ss,ax
mov eax,cr0
and al,11111110b
mov cr0,eax
JUMP16 <SEG Real>,<OFFSET Real>
CodeKLen = $
Virtual1 ENDP
CodeKSeg ENDS
RDataSeg SEGMENT PARA USE16
VGDTR PDesc <GDTLen-1,>
SPVar DW ?
SSVar DW ?
RDataSeg ENDS
RCodeSeg SEGMENT PARA USE16
ASSUME CS:RCodeSeg
Start PROC
ASSUME DS:GDTSeg
mov ax,GDTSeg
mov ds,ax
mov bx,16
mov ax,CodeKSeg
mul bx
mov CodeK.BaseL,ax
mov CodeK.BaseM,dl
mov CodeK.BaseH,dh
mov ax,LDTSeg
mul bx
mov LDTable.BaseL,ax
mov LDTable.BaseM,dl
mov LDTable.BaseH,dh
ASSUME DS:RDataSeg
mov ax,RDataSeg
mov ds,ax
mov ax,GDTSeg
mul bx
mov WORD PTR VGDTR.Base,ax
mov WORD PTR VGDTR.Base+2,dx
cld
call Init_MLDT
mov SSVar,ss
mov SPVar,sp
lgdt QWORD PTR VGDTR
cli
mov eax,cr0
or al,1
mov cr0,eax
JUMP16 <CodeK_Sel>,<OFFSET Virtual1>
Real:
mov ax,RDataSeg
mov ds,ax
lss sp,DWORD PTR SPVar
sti
mov ax,4c00h
int 21h
Start ENDP
Init_MLDT PROC
push ds
mov ax,LDTSeg
mov ds,ax
mov cx,LDNum
mov si,OFFSET LDT
InitL: mov ax,[si].BaseL
movzx eax,ax
shl eax,4
shld edx,eax,16
mov [si].BaseL,ax
mov [si].BaseM,dl
mov [si].BaseH,dh
add si,SIZE Desc
loop InitL
pop ds
ret
Init_MLDT ENDP
RCodeSeg ENDS
END Start
2.关于实例三的说明
有些步骤的实现方法已在前面的实例中做过介绍,下面就任务内无特权级变换的转移和使用局部
描述符LDT等作些说明:
(1)实模式下初始化LDT
演示任务使用了局部描述符表LDT,本实例中该LDT在实模式下初始化(当然,也可以在使用LDT前
的保护模式初始化)。为了简便,LDT中各描述符的界限和属性值在定义时预置,利用一个子程序
设置各段的段基地址。为方便起见,在定义时把各段的段值安排在相应描述符的段基地址低16位
字段中。由于实例中各段在实模式下定位(这是因为程序是从实模式下启动执行的),所以把段值
乘以16就是对应的段基地址。
(2)装载LDTR寄存器
在使用LDT之前,还要装载局部描述符表寄存器LDTR。本实例中的如下两条指令用于装载LDTR:
mov ax,LDT_SEL
lldt ax
LLDT指令是专门用于装载LDTR的指令。该指令的操作数是对应LDT段描述符的选择子。根据该选择子,
处理器从GDT中取出相应的LDT段描述符,在进行合法性等检查后,LDT段描述符的基地址和界限等信息
被装入LDTR的高速缓冲寄存器中。由于要引用GDT,所以不能在实模式下装载LDTR。在“操作系统类指
令”一文中将对LLDT指令作详细说明。
(3)利用段间转移指令JMP实现任务内无特权级的转移
在本实例中进入保护方式后,特权级是0。通过如下段间直接转移指令实现从代码段K到代码段L的转移:
JUMP16 CodeL_Sel,Virtual2
其中,选择子CodeL_Sel是对应代码段L的描述符的选择子。该描述符在LDT中,所以选择子中的描述
符表指示位TI为1。描述符特权级是0,表示对应代码段的特权级是0,选择子中的请求特权级RPL也
是0。目标代码段不是一致代码段,所以在CPL=DPL,RPL<=DPL的情况下,顺利进行相同特权级的转
移:目标代码段的选择子CodeL_Sel被装入CS,对应描述符中的信息被装入高速缓冲寄存器中,偏移
量Virtual2被装入指令指针寄存器。由于是16位代码段,所以偏移用16位表示。
类似地,通过如下段间直接转移指令实现从代码段L转移到代码段K:
JUMP16 CodeK_Sel,Virtual3
其中,选择子CodeK_Sel是对应代码段K的描述符选择子。由于描述符在GDT中,所以选择子中的TI位是0。
(4)利用段间调用指令CALL实现任务内无特权级变换的转移
在代码段L中,通过段间直接调用指令CALL调用了在代码段C中的两个子程序,这些调用都是无特权级变
换的转移。例如,利用如下指令调用了显示字符串子程序DispMsg:
CALL16 CodeC_Sel,DispMsg
其中,CodeC_Sel是代码段C的选择子,DispMsg表示子程序的入口。描述代码段C的描述符在LDT中,
描述符特权级DPL是0,所以使用的选择子CodeC_Sel的请求特权级RPL是0,描述符表指示位TI为1。
目标代码段C不是一致代码段,所以在CPL=DPL,RPL<=DPL的情况下,顺利进行相同特权级的转移:
当前CS和IP压入堆栈,目标代码段的选择子CodeC_Sel被装入CS,对应描述符中的信息被装入高速缓
冲寄存器中,16位偏移DispMsg被装入指令指针IP。由于是16位段,所以偏移用16位表示,压入堆栈
的是字而不是双字。
(5)段间返回指令RET实现任务内无特权级变换的转移
段间返回指令RET从堆栈的栈顶弹出返回地址(由选择子和偏移)构成。弹出选择子内的RPL=CPL,并
且对应DPL=CPL,RPL<=DPL是当然的,所以能顺利进行相同特权级的转移。
3.别名技术
在上述实例三中,使用了两个描述符来描述演示任务的LDT段。段描述符LDTable被安排在GDT中,
它是系统段描述符,把段LDTSeg描述成演示任务的局部描述符表LDT。描述符ToLDT被安排在LDT中,
它是数据段描述符,把段LDTSeg描述成一个普通数据段。描述符LDTable被装载到LDTR,描述
符ToLDT被装载到某个数据段寄存器。为什么要这样处理呢?根据实例三的功能要求,需要访问演示
任务的局部描述符表LDT段,以取得代码段L的段界限值,这需要通过某个段寄存器进行,
但不能把系统段描述符的选择子装载到段寄存器,所以采
用两个描述符来描述段LDTSeg。
这种为了满足对同一个段实施不同方式操作的需要,而用多个描述符加以描述的技术称为别名技术。
在保护方式程序设计中,常常要采用别名技术。例如:用两个具有不同类型值的描述符来描述同一个
段。再如,用两个具有不同DPL的描述符来描述同一个段。
<三>任务内不同特权级的变换
在一个任务内可以存在四种特权级,所以常常会发生不同特权级之间的变换。例如,外层的应用程
序调用内层操作系统的例程,以获得必要的诸如存储器分配等系统服务。内层操作系统的例程完成
后,返回到外层应用程序。
在同一任务内,实现特权级从外层到内层变换的普通途径是使用段间调用指令CALL,通过调用门进
行转移;实现特权级从内层向外层变换的普通途径是使用段间返回指令RET。注意,不能用JMP指令
实现任务内不同特权级的变换。
1.通过调用门的转移
当段间转移指令JMP和段间调用指令CALL所含指针的选择子指示调用门描述符时,就可以实现通过
调用门的转移。但只有CALL指令能变换到内层的特权级,JMP指令只能转移到同级的代码。
调用门描述符转移的入口点包含目标地址的段及偏移量的48位全指针。在执行通过任务门的段间转
移指令JMP或段间调用指令CALL时,指令所含指针内的选择子用
于确定调用门,而偏移被丢弃;把调用门内的48位全指针作为目标地址指针进行转移。
处理器采用与访问数据段相同的特权级规则控制对门描述符的访问。调用门描述符的DPL规定了访
问该门的最外层特权级,在取出调用门内的48位全指针,把它作为目标地址指针向目标代码段转移
之前,要进行特权级检查。只有在相同级或者更内层特权级的程序才可访问调用门,即CPL<=调用
门的DPL。同时,还要求指示门的选择子的RPL必须满足RPL<=调
用门的DPL的条件。检测通过后,才开始向目标代码段转移的步骤。其中还要检测目标描述
符是否为代码段描述符,调用门内的选择子指示的描述符必须是代码段描述符。此外,在装载代码
段描述符高速缓冲寄存器之前调整代码段选择子的RPL=0,也即调用门中代码段选择子的RPL被忽略。
在装载CS高速缓冲寄存器时,还要对目标代码段描述符进行保护检测。检测过程中的DPL不再是调
用门的DPL,而是调用门内选择子所指示的目标代码段描述符的DPL。段间调用指令CALL和段间转移
指令JMP所做的检测不一样。
对于使用调用门的段间转移指令JMP,检测条件与段间直接转移相同。由于已置RPL=0,所以可认为
RPL<=DPL的条件总能满足。所以,对于普通的非一致代码段,当CPL=DPL时,发生无特权级变换的
转移;对于一致代码段,在满足CPL>=DPL时也发生无特权级变换的转移;其它情形就引起异常。
对于使用调用门的段间调用指令CALL,情形就不同了。由于已置RPL=0,所以可认为RPL<=DPL的条
件总能满足。对于一致代码段,在满足CPL>=DPL时发生无特权级变换的转移。对于非一致代码段,
当CPL=DPL时,仍发生无特权级变换的转移;当CPL>DPL时,就发生向内层特权级变换的转移,将调
用门中的选择子和偏移装入CS和指令指针EIP中,并使CPL保持等于DPL,同时切换到内层堆栈。
综上所述,使用段间调用指令CALL,通过调用门可以实现从外层程序调用进入内层程序(JMP指令只
能实现无特权级变换的转移);通过调用门也可实现无特权级变换的转移。需要注意的是,JMP指令
和CALL指令都不能实现向外层特权级的转移否则会引起异常。
当然,CALL指令在最后把目标代码段的指针装入CS和EIP之前,要把原CS和EIP,即返回地址保存到
堆栈。如无特权级变换,堆栈保持不变,返回地址就保存在原堆栈中;否则,返回地址保存在内层
堆栈中。
2.堆栈切换
在使用CALL指令通过调用门向内层转移时,不仅特权级发生变换,控制转移到一个新的代码段,而
且也切换到内层的堆栈段。从本教程第五篇的任务状态段TSS的格式可见,TSS中包含有指
向0级、1级和2级堆栈的指针。在特权级发生向内层变换时,根据变换到的特权级使用TSS中相应的
堆栈指针对SS及ESP寄存器进行初始化,建立起一个空栈。
在建立起内层堆栈后,处理器先把外层堆栈的指针SS及ESP寄存器的值压入内层堆栈,以使得相应
的向外层返回可恢复原来的外层堆栈。然后,从外层堆栈复制以双字为单位的调用参数到内层堆栈
中,调用门中的DCOUNT字段值决定了复制参数的数量。这些被复制的参数是主程序通过堆栈传递给
子程序的实参,在调用之前被压入外层堆栈。通过复制栈中的参数,使内层的子程序不需要考虑堆
栈的切换,而容易地访问主程序传递过来的实参。最后,调用的返回地址被压入堆栈,以便在调用
结束时返回。下图为在向内层变换时,建立内层堆栈,并从外层堆栈复制2个双字参数到内层堆栈
的示意图。图中每项是双字,可见的段寄存器内的选择子被扩展成32为存入堆栈,高16位为0。对
于16位的使用调用门的段也是如此。
需要注意的是,无论是否通过调用门,只要不发生特权级变换,就不会切换堆栈。
3.向外层返回
与使用CALL指令通过调用门向内层变换相反,使用RET指令实现向外层返回。段间返回指令RET从
堆栈中弹出返回地址,并且可以采用调整ESP的方法,跳过相应的在调用之前压入堆栈的参数。返
回地址的选择子指示要返回的代码段的描述符,从而确定返回的代码段。选择子的RPL确定返回后
的特权级,而不是对应描述符的DPL,这是因为,段间返回指令RET可能使控制返回到一致代码段,
而一致代码段可以在DPL规定的特权级以外的特权级执行。需要注意的是,RET指令所使用的返回
地址的选择子只能使用代码段描述符,而不能使用任何系统段描述符或门描述符,当然,更不能
使用数据段描述符,否则会引起异常。与CALL指令相对应,RET指令也不能向内层返回。
段间返回指令完成返回的步骤如下:
(1)RET指令先从堆栈中弹出返回地址。如果弹出地址的选择子的RPL规定相对于CPL更外层的特权
级,那么就引起向外层返回。
(2)为向外层返回,跳过内层堆栈中的参数,再从内层栈中弹出指向外层堆栈的指针,并装入SS及
ESP,以恢复外层堆栈。
(3)调整ESP,跳过在相应的调用之前压入到外层堆栈的参数。即返回指令不但弹出内层栈的参数,
而且也弹出外层栈的参数。
(4)然后,检查数据段寄存器DS、ES、FS及GS,以保证寻址的段在外层是可访问的,如果段寄存器
寻址的段在外层是不可访问的,那么装入一个空选择子,以避免在返回时发生保护空洞。
(5)返回外层继续执行。
上述五步是对带立即数的段间返回指令而言的,立即数规定了堆栈中要跳过的参数的字节数。对于
无立即数的段间返回指令缺少第二步和第三步。若RET指令不需要向外层返回,那么就只
有(1)和(5)两步。对于有通过堆栈传递参数的子程序,必须使用带立即数的返回指令返回,否则返
回时会装载错误的外层栈指针。
若不使用带立即数的返回指令,可以在返回前把外层栈的栈指针存入内层栈中的用于保存返回地址
上方两个双字的区域中,由外层返回的过程可知,这可正确恢复外层栈的指针,但在外层程序中,
必须人为调整外层栈指针,以便废除在外层栈中压入的参数。在使用C调用约定的程序中可使用此
方法。但这会增加代码的长度和处理时间,使代码效率变低。正因为如此,在Windows 9X下,新增
加了一种STDCALL的调用约定,它正是为了适应Intel系列处理器的体系结构而产生的。
<四>演示任务内特权级变换的实例(实例四)
下面给出一个演示任务内特权级变换的实例。该实例演示在任务内通过调用门从外层特权级变换到
内层特权级;也演示通过段间返回指令从内层特权级变换到外层特权级;还演示通过调用门的无特
权级变换的转移。实例使用了任务状态段TSS,这是因为任务内特权级变换时要使用的内层堆栈指
针存放在TSS中。
1.实现步骤
该实例的实现步骤为:
(1)实方式下初始化;
(2)切换到保护模式;
(3)设置TR和LDTR。由于在任务内发生特权级变换时要切换堆栈,而内层堆栈的指针存放在当前任
务的TSS中,所以在进入保护模式后设置任务状态段寄存器TR。由于演示任务使用了局部描述符表
,所以设置LDTR;
(4)经调用门进入32位过渡代码段;
(5)建立返回3级演示代码段的环境;
(6)利用RET指令转移到3级的演示代码段。为了演示外层程序通过调用门调用内层程序,要
使CPL>0。实例先通过段间返回指令RET从特权级0变换到特权级3的演示代码段。在特权级3下,
通过调用门调用1级的子程序。随着执行段间返回指令RET,又回到3级的演示代码段;
(7)在3级的演示代码段中,经调用门转移到0级的32位过渡代码段;
(8)直接转0级的临时代码段;
(9)准备返回实模式;
(10)切换回实模式;
(11)实模式下的恢复工作。
2.源程序组织和清单
实例四由如下部分组成:
(1)全局描述符表GDT。GDT含有演示任务的TSS段描述符和LDT段描述符,此外还含有临时代码段
的描述符、规范数据段描述符和视频缓冲区段描述符。
(2)演示任务的LDT段。它含有除临时代码段外的其它代码段的描述符和演示任务各级堆栈段描
述符,还含有3个调用门。
(3)演示任务的TSS段。
(4)演示任务的0级、1级和3级堆栈段。
(5)显示子程序段。32位代码段,特权级1。
(6)演示代码段。32位代码段,特权级3。
(7)过渡代码段。32位段,特权级0。
(8)临时代码段。16位段,特权级0。
(9)实模式下的数据和代码段。
该实例的逻辑功能是显示演示代码段执行时的当前特权级CPL。源程序清单如下:
INCLUDE 386SCD.INC
GDTSeg SEGMENT PARA USE16
GDT LABEL BYTE
DUMMY Desc <>
Normal Desc <0ffffh,,,ATDW,,>
VideoBuf Desc <07fffh,8000h,0bh,ATDW+DPL3,,>
EFFGDT LABEL BYTE
DemoTSS Desc <DemoTssLen-1,DemoTSSSeg,,AT386TSS,,>
DemoLDTD Desc <DemoLDTLen-1,DemoLDTSeg,,ATLDT,,>
TempCode Desc <0ffffh,TempCodeSeg,,ATCE,,>
GDTLen = $-GDT
GDNum = ($-EFFGDT)/(SIZE Desc)
Normal_Sel = Normal-GDT
Video_Sel = VideoBuf-GDT
DemoTSS_Sel = DemoTSS-GDT
DemoLDT_Sel = DemoLDTD-GDT
TempCode_Sel = TempCode-GDT
GDTSeg ENDS
DemoLDTSeg SEGMENT PARA USE16
DemoLDT LABEL BYTE
DemoStack0 Desc <DemoStack0Len-1,DemoStack0Seg,,ATDW+DPL0,D32,>
DemoStack1 Desc <DemoStack1Len-1,DemoStack1Seg,,ATDW+DPL1,D32,>
DemoStack3 Desc <DemoStack3Len-1,DemoStack3Seg,,ATDW+DPL3,,>
DemoCode Desc <DemoCodeLen-1,DemoCodeSeg,,ATCE+DPL3,D32,>
T32Code Desc <T32CodeLen-1,T32CodeSeg,,ATCE,D32,>
EchoSubR Desc <EchoSubRLen-1,EchoSubRSeg,,ATCER+DPL1,D32,>
DemoLDNum = ($-DemoLDT)/(SIZE Desc)
DemoStack0_Sel = DemoStack0-DemoLDT+TIL+RPL0
DemoStack1_Sel = DemoStack1-DemoLDT+TIL+RPL1
DemoStack3_Sel = DemoStack3-DemoLDT+TIL+RPL3
DemoCode_Sel = DemoCode-DemoLDT+TIL+RPL3
T32Code_Sel = T32Code-DemoLDT+TIL
Echo_Sel1 = EchoSubR-DemoLDT+TIL+RPL1
Echo_Sel3 = EchoSubR-DemoLDT+TIL+RPL3
ToT32GateA Gate <T32Begin,T32Code_Sel,,AT386CGate,>
ToT32GateB Gate <T32End,T32Code_Sel,,AT386CGate+DPL3,>
ToEchoGate Gate <EchoSub,Echo_Sel3,,AT386CGate+DPL3,>
DemoLDTLen = $-DemoLDT
ToT32A_Sel = ToT32GateA-DemoLDT+TIL
ToT32B_Sel = ToT32GateB-DemoLDT+TIL
ToEcho_Sel = ToEchoGate-DemoLDT+TIL
DemoLDTSeg ENDS
DemoTSSSeg SEGMENT PARA USE16
DD 0
DD DemoStack0Len
DD DemoStack0_Sel
DD DemoStack1Len
DD DemoStack1_Sel
DD 0
DD 0
DD 0
DD 0
DD 0
DD 0
DD 0
DD 0
DD 0
DD 0
DD 0
DD 0
DD 0
DD 0
DD 0
DD 0
DD 0
DD 0
DD 0
DD DemoLDT_Sel
DW 0
DW $+2
DW 0ffffh
DemoTSSLen = $
DemoTSSSeg ENDS
DemoStack0Seg SEGMENT DWORD STACK USE32
DemoStack0Len = 512
DB DemoStack0Len DUP(?)
DemoStack0Seg ENDS
DemoStack1Seg SEGMENT DWORD STACK USE32
DemoStack1Len = 512
DB DemoStack1Len DUP(?)
DemoStack1Seg ENDS
DemoStack3Seg SEGMENT DWORD STACK USE16
DemoStack3Len = 512
DB DemoStack3Len DUP(?)
DemoStack3Seg ENDS
EchoSubRSeg SEGMENT PARA USE32
ASSUME CS:EchoSubRSeg
Message DB 'CPL=',0
EchoSub PROC FAR
cld
push ebp
mov ebp,esp
mov ax,Echo_Sel1
mov ds,ax
mov ax,Video_Sel
mov es,ax
mov edi,1996
mov esi,OFFSET Message
mov ah,4eh
EchoSub1: lodsb
or al,al
jz EchoSub2
stosw
jmp EchoSub1
EchoSub2: mov eax,[ebp+8]
and al,3
add al,'0'
mov ah,4eh
stosw
pop ebp
retf
EchoSub ENDP
EchoSubRLen = $
EchoSubRSeg ENDS
DemoCodeSeg SEGMENT PARA USE32
ASSUME CS:DemoCodeSeg
DemoBegin PROC FAR
CALL32 ToEcho_Sel,0
CALL32 ToT32B_Sel,0
DemoBegin ENDP
DemoCodeLen = $
DemoCodeSeg ENDS
T32CodeSeg SEGMENT PARA USE32
ASSUME CS:T32CodeSeg
T32Begin PROC FAR
mov ax,DemoStack0_Sel
mov ss,ax
mov esp,DemoStack0Len
push DWORD PTR DemoStack3_Sel
push DemoStack3Len
push DWORD PTR DemoCode_SEL
push OFFSET DemoBegin
retf
T32Begin ENDP
T32End PROC FAR
JUMP32 TempCode_Sel,<OFFSET ToReal>
T32End ENDP
T32CodeLen = $
T32CodeSeg ENDS
TempCodeSeg SEGMENT PARA USE16
ASSUME CS:TempCodeSeg
Virtual PROC FAR
mov ax,DemoTSS_Sel
ltr ax
mov ax,DemoLDT_Sel
lldt ax
JUMP16 ToT32A_Sel,0
ToReal: mov ax,Normal_Sel
mov ds,ax
mov es,ax
mov fs,ax
mov gs,ax
mov ss,ax
mov eax,cr0
and al,11111110b
mov cr0,eax
JUMP16 <SEG Real>,<OFFSET Real>
Virtual ENDP
TempCodeLen = $
TempCodeSeg ENDS
RDataSeg SEGMENT PARA USE16
VGDTR PDesc <GDTLen-1,>
SPVar DW ?
SSVar DW ?
RDataSeg ENDS
RCodeSeg SEGMENT PARA USE16
ASSUME CS:RCodeSeg,DS:RDataSeg
Start PROC
mov ax,RDataSeg
mov ds,ax
cld
CALL InitGDT
mov ax,DemoLDTSeg
mov fs,ax
mov si,OFFSET DemoLDT
mov cx,DemoLDNum
CALL InitLDT
mov SSVar,ss
mov SPVar,sp
lgdt QWORD PTR VGDTR
cli
mov eax,cr0
or al,1
mov cr0,eax
JUMP16 <TempCode_Sel>,<OFFSET Virtual>
Real: mov ax,RDataSeg
mov ds,ax
lss sp,DWORD PTR SPVar
sti
mov ax,4c00h
int 21h
Start ENDP
InitGDT PROC
push ds
mov ax,GDTSeg
mov ds,ax
mov cx,GDNum
mov si,OFFSET EFFGDT
InitG: mov ax,[si].BaseL
movzx eax,ax
shl eax,4
shld edx,eax,16
mov WORD PTR [si].BaseL,ax
mov BYTE PTR [si].BaseM,dl
mov BYTE PTR [si].BaseH,dh
add si,SIZE Desc
loop InitG
pop ds
mov bx,16
mov ax,GDTSeg
mul bx
mov WORD PTR VGDTR.Base,ax
mov WORD PTR VGDTR.Base+2,dx
ret
InitGDT ENDP
InitLDT PROC
ILDT: mov ax,WORD PTR FS:[si].BaseL
movzx eax,ax
shl eax,4
shld edx,eax,16
mov WORD PTR fs:[si].BaseL,ax
mov BYTE PTR fs:[si].BaseM,dl
mov BYTE PTR fs:[si].BaseH,dh
add si,SIZE Desc
loop ILDT
ret
InitLDT ENDP
RCodeSeg ENDS
END Start
3.关于实例四的说明
程序中部分片段的背景和实现方法在前面的实例中做过介绍,下面主要就如何实现任务内特权级变换
做些说明:
(1)通过段间返回指令实现特权级变换
实例在两处使用段间返回指令实现任务内的特权级变换。一处是在0级的过渡代码段中用段间RET指令
从特权级0变换到特权级3的演示代码段。该处RET指令并不对应CALL指令。实例从实模式切换到保护
模式后CPL=0。为了演示如何通过调用门调用内层程序,要设法使CPL>0。为此,实例先建立一个已发
生的从外层到内层变换的环境,即按上图所示在当前堆栈(0级堆栈)中放入外层堆栈的指针和外层演
示程序的入口指针,形成一个如下图所示的0级堆栈,无需传递参数。然后,执行段间返回指令RET,
从堆栈中弹出3级演示代码段的选择子,RPL=3,而当时CPL=0,所以导致向外层变换特权级,从0级的
过渡代码段变换到3级的演示代码段,同时切换到3级堆栈。
另一处是从1级的显示子程序EchoSub返回到3级的演示程序段。该处的RET指令与演示程序中使用的通
过调用门的段间调用指令CALL相对应,执行段间返回指令RET时的1级堆栈也如上图所示,其中的返回
地址和外层栈指针由CALL指令压入。
(2)通过调用门实现特权级变换
实例在两处使用了段间调用指令,通过调用门实现特权级的变换。一处是3级演示代码通过调用
门ToEchoGate调用1级的显示子程序。调用门ToEchoGate自身的DPL=3,只有这样,3级的演示代码才
能够使用该调用门。由于调用门内的选择子Echo_Sel3所指示的显示子程序代码段描述符DPL=1,而当
时CPL=3,所以引起从外层特权级向内层特权级的变换,使CPL=1。同时形成如上图所示的1级堆栈。
虽然调用门内的选择子Echo_Sel3的RPL=3,大于目标代码段的DPL,但没有关系,因为在通过调用门
转移时,门内指示目标代码段的选择子RPL总被作0对待。
另一处是3级演示代码还通过调用门ToT32GateB调用了0级的过渡代码。该处使用的调用门描述符DPL也
等于3。由于调用门内的选择子T32Code_Sel所指示的过渡代码段描述符的DPL=0,而当时CPL=3,所以
引起从3特权级向0特权级的变换,使CPL=0。同时形成如上图所示的0级堆栈。但该处的调用实际上是
“有去无回”的,调用的目的是转移到0级的过渡代码,准备返回实模式。由于从3级的演示代码到0级
的过渡代码要发生特权级变换,所以不能使用转移指令JMP,必须使用调用指令CALL。
(3)通过调用门实现无特权级变换
在临时代码段中,使用调用门ToT32GateA转移到过渡代码段。尽管调用门内的选择子T32Code_Sel所指
示的过渡代码段描述符的DPL=0,但当时CPL=0,所以不发生特权级变换。正是这个原因,才可以使用
段间转移指令JMP。
(4)子程序EchoSub的实现
子程序EchoSub的功能是显示调用程序执行时的特权级。调用程序的执行特权级在代码段寄存器CS内选
择子的RPL字段,在调用EchoSub时,CS寄存器的内容被压入堆栈。子程序从堆栈取得调用程序的代码
段选择子,再从中分离出RPL就可得调用程序的执行特权级。
(5)装载任务状态段寄存器TR
在任务内发生特权级变换时堆栈也随着自动切换,外层堆栈指针保存在内层堆栈中,而内层堆栈指针
存放在当前任务的TSS中。所以,在从外层向内层变换时,要访问TSS(从内层向外层转移时不需要访
问TSS,而只需内层栈中保存的栈指针)。实例在进入保护模式下的临时代码段后,通过如下两条指令
装载任务状态段寄存器TR,使其指向已预置好的任务的TSS:
mov ax,DemoTSS_Sel
ltr ax
LTR指令是专门用于装载任务状态段寄存器TR的指令。该指令的操作数是对应TSS段描述符的选择
子。LTR指令从GDT中取出相应的TSS段描述符,把TSS段描述符的基地址和界限等信息装入TR的高速
缓冲寄存器中。
<五>任务切换
利用段间转移指令JMP或者段间调用指令CALL,通过任务门或直接通过任务状态段,可以切换到别的
任务。此外,在中断/异常或者执行IRET指令时也可能发生任务切换。需要注意的是,因为RET指令
的目标地址只能使用代码段描述符,所以,不能通过RET指令实现任务切换。
1.直接通过TSS进行任务切换
段间转移指令JMP或段间调用指令CALL所含指针的选择子指示一个可用任务状态段TSS描述符时,
正常情况下就发生从当前任务到由该可用TSS对应任务(目标任务)的切换。目标任务的入口点由
目标任务TSS内的CS和EIP字段所规定的指针确定。这样的JMP或CALL指令内的偏移被丢弃。另外,
对于段间调用指令CALL,若目标选择子指示一TSS段描述符或任务门时,则返回地址和外层栈指
针并不压入堆栈。
处理器采用与访问数据段相同的特权级规则控制对TSS段描述符的访问。TSS段描述符的DPL规定
了访问该描述符的最外层特权级,只有在相同级别或更内层级别的程序才可以访问它。同时,还
要求指示它的选择子的RPL必须满足RPL<=TSS的DPL的条件。当这些条件满足时,就开始任务切换。
2.通过任务门进行任务切换
任务门内的选择子指示某个任务的TSS描述符。当段间转移指令JMP或段间调用指令CALL所含指针
的选择子指示一个任务门时,正常情况下就发生任务切换,即从当前任务切换到由任务门内的选
择子所指示的TSS描述符对应的任务(目标任务)。这样的JMP或CALL指令内的偏移被丢弃;任务门
内的偏移也无意义。
处理器采用与访问数据段相同的特权级规则控制对任务门的访问。任务门的DPL规定了访问该任
务门的最外层特权级,只有在同级或更内层级别的程序才可以访问它。同时,还要求指示任务门
的选择子RPL必须满足RPL<=任务门的DPL的条件。在这些条件满足时,再检查任务门内的选择子,
要求该选择子指示GDT中的可用的TSS描述符。对于任务门所指向的TSS描述符的DPL不进行特权级
检查。检查通过以后,就开始任务切换。
3.任务切换过程
根据指示目标任务TSS描述符的选择子进行任务切换的一般过程如下:
第一,测试目标任务状态段的界限。TSS用于保存任务的各种状态信息,不同的任务,TSS中可以
有数量不等的其他信息,根据任务状态段的基本格式,TSS的界限应大于或等于103(104-1)。
第二,把寄存器现场保存到当前任务的TSS。把通用寄存器、段寄存器、EIP及EFLAGS的当前值保
存到当前TSS中。保存的EIP值是返回地址,指向引起任务切换指令的下一条指令。但不
把LDTR和CR3的内容保存到TSS中。
第三,把指示目标任务TSS的选择子装入TR寄存器中。同时把对应TSS的描述符装入TR的高速缓冲
寄存器中。此后,当前任务改称为原任务,目标任务改称为当前任务。
第四,基本恢复当前任务(目标任务)的寄存器现场。根据保存在TSS中的内容,恢复各通用寄存
器、段寄存器、EFLAGS及EIP。在装入寄存器的过程中,为了能正确地处理可能发生的异常,只
把对应选择子装入各段寄存器。此时选择子的P位为0。还装载CR3寄存器。
第五,进行链接处理。如果需要链接,那么将指向原任务TSS的选择子写入当前任务TSS的链接字
段,把当前任务TSS描述符类型改为“忙”(并不修改原任务状态段描述符的“忙”位),并将标
志寄存器EFLAGS中的NT位置1,表示是嵌套任务。如果需要解链,那么把原任务TSS描述符类型改
为“可用”。如果无解链处理,那么将原任务TSS描述符类型置为“可用”,当前任务TSS描述符
类型置为“忙”。由于JMP指令引起的任务切换不实施链接/解链处理;由CALL指令、中断、IRET指
令引起的任务切换要实施链接/解链处理。
第六,把CR0中的TS标志置为1,这表示已发生过任务切换,在当前任务使用协处理器指令时,产生
自陷。由自陷处理程序完成有关协处理器现场的保存和恢复。这有利于快速地进行任务切换。
第七,把TSS中的CS选择子的RPL作为当前任务特权级设置为CPL。又因为装入CS高速缓冲寄存器时
要检测CPL=代码段描述符的DPL,所以TSS中的选择子所指示的代码段描述符的DPL必须等于该选择
子的RPL。任务切换可以在一个任务的任何特权级发生,并且可以切换到另一任务的任何特权级。
第八,装载LDTR寄存器。一个任务可以有自己的LDT,也可以没有。当任务没有LDT时,TSS中LDT选
择子为0。如果TSS中LDT选择子非空,则从GDT中读出对应的LDT描述符,在经过测试后,把所读
的LDT描述符装入LDTR高速缓冲寄存器。如果LDT选择子为空,则将LDT的存在位置为0,表明任务不
使用LDT。
第九,装载代码段寄存器CS、堆栈段寄存器SS和各数据段寄存器及其高速缓冲寄存器。在装入代码
段高速缓存之前,也要进行特权检查,处理器调整TSS中的CS选择子的RPL=0,装入之后,调
整CS的RPL等于目标代码段的DPL。堆栈段使用的是TSS中的SS和SP字段的值,而不是使用内层栈保
存区中的指针,即使发生了向内层特权级的变换。这与任务内的通过调用门的转移不同。
第十,把调试寄存器DR7中的局部启用位置为0,以清除局部于原任务的各个断点和方式。
4.关于任务状态和嵌套的说明
需要注意的是,任务切换不能递归。
在段间转移指令JMP引起任务切换时,不实施链接,不导致任务的嵌套。它要求目标任务是可用任
务。切换过程中把原任务置为“可用”,目标任务置为“忙”。
在段间调用指令CALL引起任务切换时,实施链接,导致任务的嵌套。它要求目标任务是可用的任务。
在切换过程中把目标任务置为“忙”,原任务仍保持“忙”;标志寄存器EFLAGS中的NT位被置为1,
表示任务是嵌套任务。
在由中断异常引起任务切换时,实施链接,导致任务的嵌套。要求目标任务是可用的任务。在切换
过程中把目标任务置为“忙”,原任务仍保持“忙”;标志寄存器EFLAGS中的NT位被置为1,表示任
务是嵌套任务。
在执行IRET指令时引起任务切换,那么实施解链。要求目标任务是忙的任务。在切换过程中把原任
务置为“可用”,目标任务仍保持“忙”。关于中断/异常如何引起任务切换和指令IRET如何考虑任
务切换的内容将在后面的文章中论述。
<六>演示任务切换的实例(实例五)
下面给出一个用于演示任务切换的实例。该实例的逻辑功能是在切换后显示原任务的挂起点(EIP)的
值。该实例演示内容包括:直接通过TSS段的任务切换,通过任务门的任务切换,任务内特权级的变
换及参数传递。
1.实现步骤
为了达到演示任务切换和特权级变换的目的,实例五在保护方式下涉及到两个任务,一个任务称为
临时任务,另一个任务称为演示任务。演示任务的功能是演示通过调用门实现特权级的变换和堆栈
间参数的自动复制。临时任务和演示任务配合展示任务切换。实例五的主要实现步骤如下:
(1)实模式下初始化;
(2)切换到保护模式;
(3)设置TR对应临时任务,特权级为0;
(4)直接切换到演示任务,演示任务的特权级为2;
(5)把入口参数压入堆栈,经调用门进入显示信息子程序,显示信息子程序的特权级为0;
(6)从堆栈中取出入口参数并处理;
(7)从显示信息子程序返回特权级为2的演示代码段;
(8)为切换回实模式作部分准备工作;
(9)经任务门切换到特权级为0的临时任务;
(10)准备返回实模式;
(11)切换到实模式;
(12)实模式下的恢复工作。
在任务切换时,把原任务的现场保存到TR所指示的TSS内,然后再把指向目标任务的TSS描述符的
选择子装入TR,所以在从临时任务切换到演示任务之前,要把指向临时任务TSS描述符的选择子
装入TR。通过把演示任务的TSS初始化成恢复点在特权级2的代码段,使得从临时任务切换到演示
任务后,当前特权级CPL=2。
2.源程序组织和清单
实例五由如下部分组成:
(1)全局描述符表GDT。GDT含有演示任务的TSS描述符和LDT段描述符,还含有临时任务TSS描述符
和临时任务的代码段描述符,此外,还含有子程序代码段描述符、规范数据段描述符和视频缓冲
区段描述符。
(2)演示任务的TSS。以根据演示要求初始化。
(3)演示任务的LDT段。它含有演示任务的0级和2级堆栈段描述符、代码段和数据段描述符、分别
以数据段方式描述LDT和临时任务TSS的数据段描述符、以及指向子程序的调用门和指向临时任务
的任务门。
(4)演示任务的0级和2级堆栈段。32位段,特权级分别为0和2。
(5)演示任务数据段。32位段,特权级3。
(6)子程序代码段。32位代码段,特权级0。
(7)演示任务代码段。32位代码段,特权级2。
(8)临时任务的TSS段。未初始化。
(9)临时任务代码段。16位代码段,特权级0。
(10)实模式下的数据和代码段。
该实例的逻辑功能是在切换后显示原任务的挂起点(EIP)的值。源程序清单如下:
INCLUDE 386SCD.INC
GDTSeg SEGMENT PARA USE16
GDT LABEL BYTE
DUMMY Desc <>
Normal Desc <0ffffh,,,ATDW,,>
Normal_Sel = Normal-GDT
VideoBuf Desc <0ffffh,8000h,0bh,ATDW+DPL3,,>
Video_Sel = VideoBuf-GDT
EFFGDT LABEL BYTE
DemoLDTab Desc <DemoLDTLen-1,DemoLDTSeg,,ATLDT,,>
DemoLDT_Sel = DemoLDTab-GDT
DemoTSS Desc <DemoTSSLen-1,DemoTSSSeg,,AT386TSS,,>
DemoTSS_Sel = DemoTSS-GDT
TempTSS Desc <TempTSSLen-1,TempTSSSeg,,AT386TSS+DPL2,,>
TempTSS_Sel = TempTSS-GDT
TempCode Desc <0ffffh,TempCodeSeg,,ATCE,,>
TempCode_Sel = TempCode-GDT
SubR Desc <SubRLen-1,SubRSeg,,ATCE,D32,>
SubR_Sel = SubR-GDT
GDNum = ($-EFFGDT)/(SIZE Desc)
GDTLen = $-GDT
GDTSeg ENDS
DemoLDTSeg SEGMENT PARA USE16
DemoLDT LABEL BYTE
DemoStack0 Desc <DemoStack0Len-1,DemoStack0Seg,,ATDW,D32,>
DemoStack0_Sel = DemoStack0-DemoLDT+TIL
DemoStack2 Desc <DemoStack2Len-1,DemoStack2Seg,,ATDW+DPL2,D32,>
DemoStack2_Sel = DemoStack2-DemoLDT+TIL+RPL2
DemoCode Desc <DemoCodeLen-1,DemoCodeSeg,,ATCE+DPL2,D32,>
DemoCode_Sel = DemoCode-DemoLDT+TIL+RPL2
DemoData Desc <DemoDataLen-1,DemoDataSeg,,ATDW+DPL3,D32,>
DemoData_Sel = DemoData-DemoLDT+TIL
ToDLDT Desc <DemoLDTLen-1,DemoLDTSeg,,ATDW+DPL2,,>
ToDLDT_Sel = ToDLDT-DemoLDT+TIL
ToTTSS Desc <TempTSSLen-1,TempTSSSeg,,ATDW+DPL2,,>
ToTTSS_Sel = ToTTSS-DemoLDT+TIL
DemoLDNum = ($-DemoLDT)/(SIZE Desc)
ToSubR Gate <SubRB,SubR_Sel,,AT386CGate+DPL3,>
ToSubR_Sel = ToSubR-DemoLDT+TIL+RPL2
ToTempT Gate <,TempTSS_Sel,,ATTaskGate+DPL3,>
ToTempT_Sel = ToTempT-DemoLDT+TIL
DemoLDTLen = $-DemoLDT
DemoLDTSeg ENDS
DemoTSSSeg SEGMENT PARA USE16
DD 0
DD DemoStack0Len
DW DemoStack0_Sel,0
DD 0
DW 0,0
DD 0
DW 0,0
DD 0
DW DemoBegin,0
DD 0
DD 0
DD 0
DD 0
DD 0
DD DemoStack2Len
DD 0
DD 0
DD 1986
DW Video_Sel,0
DW DemoCode_Sel,0
DW DemoStack2_Sel,0
DW DemoData_Sel,0
DW ToDLDT_Sel,0
DW ToTTSS_Sel,0
DW DemoLDT_Sel,0
DW 0
DW $+2
DB 0ffh
DemoTSSLen = $
DemoTSSSeg ENDS
DemoStack0Seg SEGMENT PARA USE32
DemoStack0Len = 1024
DB DemoStack0Len DUP(0)
DemoStack0Seg ENDS
DemoStack2Seg SEGMENT PARA USE32
DemoStack2Len = 512
DB DemoStack2Len DUP(0)
DemoStack2Seg ENDS
DemoDataSeg SEGMENT PARA USE32
Message DB 'Value=',0
DemoDataLen = $
DemoDataSeg ENDS
SubRSeg SEGMENT PARA USE32
ASSUME CS:SubRSeg
SubRB PROC FAR
push ebp
mov ebp,esp
pushad
mov esi,DWORD PTR [ebp+12]
mov ah,4ah
jmp SHORT SubR2
SubR1: stosw
SubR2: lodsb
or al,al
jnz SubR1
mov ah,4eh
mov edx,DWORD PTR [ebp+16]
mov ecx,8
SubR3: rol edx,4
mov al,dl
call HToASCII
stosw
loop SubR3
popad
pop ebp
ret 8
SubRB ENDP
HToASCII PROC
and al,0fh
add al,90h
daa
adc al,40h
daa
ret
HToASCII ENDP
SubRLen = $
SubRSeg ENDS
DemoCodeSeg SEGMENT PARA USE32
ASSUME CS:DemoCodeSeg,DS:DemoDataSeg
DemoBegin PROC FAR
mov BYTE PTR fs:ToSubR.DCount,2
push DWORD PTR gs:TempTask.TREIP
push OFFSET Message
CALL32 ToSubR_Sel,0
ASSUME DS:TempTSSSeg
push gs
pop ds
mov ax,Normal_Sel
mov WORD PTR TempTask.TRDS,ax
mov WORD PTR TempTask.TRES,ax
mov WORD PTR TempTask.TRFS,ax
mov WORD PTR TempTask.TRGS,ax
mov WORD PTR TempTask.TRSS,ax
JUMP32 ToTempT_Sel,0
jmp DemoBegin
DemoBegin ENDP
DemoCodeLen = $
DemoCodeSeg ENDS
TempTSSSeg SEGMENT PARA USE16
TempTask TSS <>
DB 0ffh
TempTSSLen = $
TempTSSSeg ENDS
TempCodeSeg SEGMENT PARA USE16
ASSUME CS:TempCodeSeg
Virtual PROC FAR
mov ax,TempTSS_Sel
ltr ax
JUMP16 DemoTSS_Sel,0
clts
mov eax,cr0
and al,11111110b
mov cr0,eax
JUMP16 <SEG Real>,<OFFSET Real>
Virtual ENDP
TempCodeSeg ENDS
RDataSeg SEGMENT PARA USE16
VGDTR PDesc <GDTLen-1,>
SPVar DW ?
SSVar DW ?
RDataSeg ENDS
RCodeSeg SEGMENT PARA USE16
ASSUME CS:RCodeSeg,DS:RDataSeg,ES:RDataSeg
Start PROC
mov ax,RDataSeg
mov ds,ax
cld
call InitGDT
mov ax,DemoLDTSeg
mov fs,ax
mov si,OFFSET DemoLDT
mov cx,DemoLDNum
call InitLDT
mov SSVar,ss
mov SPVar,sp
lgdt QWORD PTR VGDTR
cli
mov eax,cr0
or al,1
mov cr0,eax
JUMP16 <TempCode_Sel>,<OFFSET Virtual>
Real: mov ax,RDataSeg
mov ds,ax
lss sp,DWORD PTR SPVar
sti
mov ax,4c00h
int 21h
Start ENDP
InitGDT PROC
push ds
mov ax,GDTSeg
mov ds,ax
mov cx,GDNum
mov si,OFFSET EFFGDT
InitG: mov ax,[si].BaseL
movzx eax,ax
shl eax,4
shld edx,eax,16
mov WORD PTR [si].BaseL,ax
mov BYTE PTR [si].BaseM,dl
mov BYTE PTR [si].BaseH,dh
add si,SIZE Desc
loop InitG
pop ds
mov bx,16
mov ax,GDTSeg
mul bx
mov WORD PTR VGDTR.Base,ax
mov WORD PTR VGDTR.Base+2,dx
ret
InitGDT ENDP
InitLDT PROC
mov ax,WORD PTR fs:[si].BaseL
movzx eax,ax
shl eax,4
shld edx,eax,16
mov WORD PTR fs:[si].BaseL,ax
mov BYTE PTR fs:[si].BaseM,dl
mov BYTE PTR fs:[si].BaseH,dh
add si,SIZE Desc
loop InitLDT
ret
InitLDT ENDP
RCodeSeg ENDS
END Start
3.关于实例五的说明
程序中部分片段的背景和实现方法在前面的实例中做过介绍,下面主要就任务切换和通过调用门
实现任务内特权级变换时参数的复制等情形做些说明:
(1)从临时任务直接通过TSS切换到演示任务
从实模式切换到保护模式后,就认为进入了临时任务。但TR并没有指向临时任务的TSS。在从临
时任务切换到演示任务时,要把临时任务的现场保存到临时任务的TSS,这就要求TR指向临时任
务的TSS。所以首先要使用LTR指令把指向临时任务TSS描述符的选择子装入TR。在利用LTR指令显
示地装载TR时,并不引用TSS的内容,所以临时任务的TSS几乎没有初始化。理由是这不是真正的
任务切换。
临时任务采用段间转移指令JMP,直接指向演示任务的TSS,切换到演示任务。在执行切换到演示
任务的段间转移指令JMP时,CPL=0,JMP指令中所含选择子内的RPL=0,演示任务TSS的描述符特
权级DPL=0,并且是一个可用的TSS,所以顺利进行从临时任务到演示任务的切换。切换过程包
括:把临时任务的执行现场保存到临时任务的TSS中;从演示任务的TSS中恢复演示任务的现场;
把演示任务的LDT描述符选择子装载到LDTR等。从源程序可见,初始化后的演示任务的TSS中CS字
段存放的选择子是DemoCode_Sel,对应的描述符在演示任务的LDT中,并且DPL=2,它描述了代码
段DemoCode;挂起点是DemoBegin,所以在切换到演示任务后从该点开始执行,并且CPL=2。由于
使用JMP指令进行任务切换,所以不实施任务链接。
(2)从演示任务通过任务门切换到临时任务
演示任务采用段间转移指令JMP,通过任务门ToTempT切换到临时任务。在执行切换到临时任务的
段间转移指令JMP时,CPL=2,JMP指令中所含选择子ToTempT_Sel内的RPL=0,它指示的任务门的
描述符特权级DPL=3,所以可以访问该任务门。任务门内的选择子TempTSS_Sel指示临时任务
的TSS,并且此时的临时任务TSS是可用的,所以可顺利进行任务切换。演示任务的现场保存到演
示任务的TSS;临时任务的现场从临时任务的TSS恢复。
临时任务的挂起点是临时任务代码段的ToRela点,所以恢复后的临时任务从该点开始,CS含临时
任务代码段的选择子。但由于在演示任务内“强硬”地改变了临时任务TSS内的SS和DS等字段,
所以在恢复到临时任务时,SS和DS等段寄存器内已含有规范数据段的选择子,而非挂起时的原有
值。注意,这种做法不被提倡,但在这里却充分地展示了如何从TSS恢复任务。
(3)演示任务内的特权级变换和堆栈传递参数
演示任务采用段间调用指令CALL,通过调用门ToSubR调用子程序SubRB。执行段间调用指令CALL时
的CPL=2,指令所含指向调用门的选择子的RPL=2,调用门的DPL=3,所以对调用门的访问是允许
的;尽管调用门内的选择子的RPL=3,但由于它所指示的子程序代码段描述符的DPL=0,所以在调
用过程中就发生了从特权级2到特权级0的变换,同时堆栈也被切换。
演示代码段通过堆栈传递了两个参数给子程序SubRB。在把参数压入堆栈时,CPL=2,使用的也是
对应特权级2 的堆栈。通过调用门进入子程序后,CPL=0,使用0级堆栈。为此,把调用门ToSubR中
的DCount字段设置为2,表示在特权级向内层变换时,需从外层堆栈依次复制2各个双字参数到内层
堆栈。随着特权级变换,堆栈也跟着变换。这种在堆栈切换的同时复制所需参数的做法,保证了子
程序方便地访问堆栈中的参数,而无需考虑是哪个堆栈。
随着从子程序SubRB的返回,CPL=0变换为CPL=2,堆栈也回到2级堆栈。由于再次进入0级堆栈,总
是从空开始,所以在返回前不是非要保持内层堆栈平衡不可。但2级堆栈中的2个双字参数需要废
除。从源程序可见,这是采用带立即数的段间返回指令实现的,在返回的同时,自动废除外层堆栈
中的参数,同时也废除了内层堆栈中的参数。
(4)别名技术的应用
关于别名技术,前文已经作过介绍。实例五也有两处应用了别名技术。
为了把调用门ToSubR中的DCount字段设置成2,使用一个数据段描述符ToDLDT描述调用门所在演示
任务的LDT段,该描述符把演示任务的LDT段描述成数据段。
还有一处是把临时任务的TSS视为普通数据段。从演示任务切换到临时任务之前,把指向描述规范
数据段的描述符Normal的选择子Normal_Sel填到临时任务TSS中的各数据段寄存器(包括堆栈段寄
存器)字段,于是在切换到临时任务时,作为恢复临时任务的现场,该选择子就被装到DS等数据段
寄存器,对应的描述符Normal内的信息也就被装入到对应的高速缓冲寄存器中,达到为从临时任
务切换到实模式作准备的目的。
参考资料 |
书 名 |
出 版 社 |
作 者 |
《保护方式下的80386及其编程》 |
清华大学出版社 |
周明德主编 |
《80X86汇编语言程序设计教程》 |
清华大学出版社 |
扬季文主编 |